###########################################################################
#
# File:         __init__.py (package: onyx)
# Date:         15-Sep-2007
# Author:       Hugh Secker-Walker
# Description:  Top-level namespace for Onyx packages
#
# This file is part of Onyx   http://onyxtools.sourceforge.net
#
# Copyright 2007 - 2009 The Johns Hopkins University
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#   http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.  See the License for the specific language governing
# permissions and limitations under the License.
#
###########################################################################

"""
Namespace for Onyx constants and exceptions

Onyx was originally developed at the Human Language Technology Center of
Excellence at Johns Hopkins University.


Package Attributes
------------------

:data:`platform` - A string encoding the runtime platform.  It consists of five parts
separated by dash: platform, os, architecture, native data-width, endianness

E.g. ``darwin-posix-i386-32bit-le``

:data:`home` - A string of the absolute path to the top-level directory of the Onyx
installation from which this onyx package was imported.

"""

import os, sys
from oproject import platform

# XXX this logic (and all uses of __file__ in the project) may want to use the
# pkg_resources API from setuptools...

# figure out where *this* file is, set up home and the platform-specific bin_path
onyx_package = 'py', 'onyx', '__init__.py'
if __name__ == onyx_package[1]:
    head = __file__
    onyx_path = list()
    while head and head[-1] != os.sep:
        head, tail = os.path.split(head)
        if not onyx_path and tail.startswith(onyx_package[-1]):
            tail = onyx_package[-1]
        onyx_path.append(tail)
    if head:
        assert head[-1] == os.sep
        onyx_path.append(head)
    assert onyx_path
    onyx_path.reverse()
    onyx_path = tuple(onyx_path)
    if onyx_path[-3:] != onyx_package:
        raise ValueError("unexpected path for onyx package file: %r" % (__file__,))

    home = os.path.join(*onyx_path[:-3])
    onyx_path = os.path.join(*onyx_path[:-1])
    #print 'home:', repr(home)
    #print 'onyx_path:', repr(onyx_path)

    # XXX bin_path may be obsolete if we're not using any binaries; right now there's just sph2pipe
    # Path to platform-dependent binaries
    bin_path = os.path.join(home, 'bin', platform)

    # Path to platform-dependent libraries loaded from Python.  We may be
    # running a source, i.e. development, directory.  If so, then we need to use
    # the build directory as the base for this path so that even those we're
    # building will be around.
    SENTINEL_NAME = "source_tree_sentinel.py"
    running_in_source_dir = os.path.exists(os.path.join(onyx_path, SENTINEL_NAME))
    pylib_home = home if not running_in_source_dir else os.path.join(home, 'build', platform)
    pylib_build_path = os.path.join(pylib_home, 'cpp', 'pylib', platform)

    ###### PROJECT-WIDE PATH CHANGES #########
    
    # Prepend the project's platform-specific bin directory to os.environ.  If
    # you change this behavior, make sure that
    # site_scons/site_tools/{ProjectTools.py,PyTools.py} etc mirror the
    # alteration to PATH for SCons sub-processes.
    os.environ['PATH'] = bin_path + os.pathsep + os.environ['PATH']

    # Prepend the project's platform-specific pylib build directory to sys.path
    # so that we find the project's shared objects loaded by Python files.
    sys.path.append(pylib_build_path)
del os, sys

# Exceptions
class OnyxException(Exception):
    """
    Baseclass for exceptions generated by logic within the project.

    This is a subclass of Python's Exception class.  Subclasses of this
    exception are raised when something unexpected happens within the logic of
    project's code.  Note: it does not replace standard Python runtime
    exceptions, e.g. TypeError and ValueError, which are raised from within the
    project when an interface is misused.

    >>> import onyx
    >>> e = onyx.OnyxException("something went wrong")
    >>> type(e), type(e).__bases__
    (<class 'onyx.OnyxException'>, (<type 'exceptions.Exception'>,))
    >>> try: raise e
    ... except onyx.OnyxException, ee:
    ...   print ee
    something went wrong
    """

class DataTypeError(OnyxException):
    """
    The DataTypeError exception is raised when the type of some data is
    incorrect or cannot be determined.

    For example, when trying to read audio data, this exception will be raised
    if the type of audio data in a file cannot be determined from the file
    header.

    >>> import onyx
    >>> e = onyx.DataTypeError()
    >>> type(e), type(e).__bases__
    (<class 'onyx.DataTypeError'>, (<class 'onyx.OnyxException'>,))
    """

class DataFormatError(OnyxException):
    """
    The DataFormatError is raised when an inconsistency is detected in the
    internal structure of some data.

    For example, while reading audio data, this exception will be raised if the
    amount of data in the file differs from what the file header promised, or if
    decoding of data fails due to corruption, etc.

    >>> import onyx
    >>> e = onyx.DataFormatError()
    >>> type(e), type(e).__bases__
    (<class 'onyx.DataFormatError'>, (<class 'onyx.OnyxException'>,))
    """

class ExternalError(OnyxException):
    """
    The ExternalError is used when an exception must be raised due to an error
    condition occuring outside of Python or the project's Python code.

    ExternalError is most often used when an error condition is reported from a
    subprocess and that error condition cannot readily be interpreted as one of
    the standard Python exceptions.

    >>> import onyx
    >>> e = onyx.ExternalError()
    >>> type(e), type(e).__bases__
    (<class 'onyx.ExternalError'>, (<class 'onyx.OnyxException'>,))
    """

class SubprocessError(EnvironmentError, ExternalError):
    """
    Exception raised when communication with a subprocess fails.

    In addition to being an ExternalError, this is a subclass of Python's
    EnvironmentError, and supports EnvironmentError's interface:
    http://docs.python.org/lib/module-exceptions.html

    >>> x = SubprocessError(1, "foo")
    >>> isinstance(x, EnvironmentError), isinstance(x, ExternalError)
    (True, True)
    >>> x.errno, x.strerror, x.filename
    (1, 'foo', None)
    >>> str(x)
    '[Errno 1] foo'

    >>> x = SubprocessError(1, "foo", "bar")
    >>> x.errno, x.strerror, x.filename
    (1, 'foo', 'bar')
    >>> str(x)
    "[Errno 1] foo: 'bar'"

    >>> x = SubprocessError(1)
    >>> x.errno, x.strerror, x.filename
    (None, None, None)
    >>> str(x)
    '1'

    >>> x = SubprocessError(1, 'foo', 'bar', 'baz')
    >>> x.errno, x.strerror, x.filename
    (None, None, None)
    >>> str(x)
    "(1, 'foo', 'bar', 'baz')"
    """


# inject these symbols into the onyx package namespace
#
# note: injection of doctestmod function replaces the binding to the doctestmod
# package
from onyx.doctestmod import doctestmod, DOCTESTMODTAG, parse_doctest_message, make_doctest_message, parse_doctest_message

def onyx_mainstartup():
    """
    Boilerplate function called explicitly by almost every Onyx module when run
    as standalone script.  It does the doctest work on a module and reports
    summary statistics.  It is called from the ``if __name__ == '__main__':``
    clause at the bottom of most modules.  It is intended that this function be
    called by modules that are also scripts, typically prior to argument parsing
    and the actual work of the script.

    If there are no command-line arguments, this function logs the result
    message, and if there were failures it calls sys.exit() with the number of
    failures.  

    If there are command-line arguments and there were no failures then nothing
    gets logged -- this silence supports the module's use as a script.  If there
    are command-line arguments and there were failures, then the result message
    is logged to both stdout and stderr, but sys.exit() is not called.

    Note: this doctest-calling function is difficult to test explicitly via the
    doctest facility, particularly its failure modes.  So there's no doctest in
    this documentation string.
    """
    import sys
    num_ok, num_bad, msg = doctestmod()
    if len(sys.argv) <= 1:
        sys.stdout.write(msg)
        sys.stdout.flush()
        if num_bad != 0:
            sys.exit(num_bad)
    if num_bad != 0:
        # note: we write to both stdout and stderr
        sys.stdout.write(msg)
        sys.stdout.flush()
        sys.stderr.write(msg)


def _test_doctest_functions():
    """
    A helper function for doctesting the module doctestmod.  Due to recursion
    issues, it is impractical for doctestmod to test itself.  This function
    exists soley so that doctest will run the tests in this documentation string
    and thereby test functions that have been imported from doctestmod.

    Tests of the message maker

    >>> make_doctest_message( __name__, 0, 0, 0.1)
    'doctestmod : __main__ : total 0  ok 0  bad 0 : pass : elapsed time 100000 usec (0.100000 seconds) \\n'

    >>> make_doctest_message( __name__, 0, 1, 0.1)
    'doctestmod : __main__ : total 1  ok 1  bad 0 : pass : elapsed time 100000 usec (0.100000 seconds) \\n'

    >>> make_doctest_message( __name__, 1, 1, 0.1)
    "doctestmod : __main__ : total 1  ok 0  bad 1 : *** fail *** : file '__main__' : elapsed time 100000 usec (0.100000 seconds) \\n"

    >>> make_doctest_message( __name__, 0, 2, 0.1)
    'doctestmod : __main__ : total 2  ok 2  bad 0 : pass : elapsed time 100000 usec (0.100000 seconds) \\n'

    >>> make_doctest_message( __name__, 1, 2, 0.1)
    "doctestmod : __main__ : total 2  ok 1  bad 1 : *** fail *** : file '__main__' : elapsed time 100000 usec (0.100000 seconds) \\n"

    >>> make_doctest_message( __name__, 2, 2, 0.1)
    "doctestmod : __main__ : total 2  ok 0  bad 2 : *** fail *** : file '__main__' : elapsed time 100000 usec (0.100000 seconds) \\n"


    Tests of the message parser

    First, the unparseable failure mode

    >>> parse_doctest_message('foo bar') is None
    True

    A parseable message, successful tests

    >>> parse = parse_doctest_message(make_doctest_message( __name__, 0, 1, 0.1))
    >>> parse is not None
    True

    >>> parse
    ('doctestmod', '__main__', 1, 1, 0, 'pass : elapsed time 100000 usec (0.100000 seconds)')
    >>> tag, module_name, total, ok, bad, passfail = parse

    >>> tag == DOCTESTMODTAG, total >= 1, ok >= 1, bad == 0
    (True, True, True, True)
    
    A parseable message, one test failed

    >>> parse_doctest_message(make_doctest_message( __name__, 1, 2, 0.1))
    ('doctestmod', '__main__', 2, 1, 1, "*** fail *** : file '__main__' : elapsed time 100000 usec (0.100000 seconds)")
    

    Ahem

    >>> _test_doctest_functions() is None
    True
    """


if __name__ == '__main__':
    from onyx import onyx_mainstartup
    onyx_mainstartup()
